/***** BEGIN LICENSE BLOCK *****
* Version: EPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Eclipse Public
* License Version 1.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/epl-v10.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby.ext.openssl;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyIO;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubyThread;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.openssl.x509store.X509Utils;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
import org.jruby.runtime.Visibility;
import static org.jruby.ext.openssl.SSL.newSSLErrorWaitReadable;
import static org.jruby.ext.openssl.SSL.newSSLErrorWaitWritable;
import static org.jruby.ext.openssl.OpenSSL.*;
/**
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
*/
public class SSLSocket extends RubyObject {
private static final long serialVersionUID = -2084816623554406237L;
private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new SSLSocket(runtime, klass);
}
};
public static void createSSLSocket(final Ruby runtime, final RubyModule SSL) { // OpenSSL::SSL
final ThreadContext context = runtime.getCurrentContext();
RubyClass SSLSocket = SSL.defineClassUnder("SSLSocket", runtime.getObject(), ALLOCATOR);
// SSLSocket.addReadAttribute(context, "io");
// SSLSocket.defineAlias("to_io", "io");
// SSLSocket.addReadAttribute(context, "context");
SSLSocket.addReadWriteAttribute(context, "sync_close");
SSLSocket.addReadWriteAttribute(context, "hostname");
SSLSocket.defineAnnotatedMethods(SSLSocket.class);
SSLSocket.undefineMethod("dup");
}
public SSLSocket(Ruby runtime, RubyClass type) {
super(runtime, type);
}
private static RaiseException newSSLError(Ruby runtime, Exception exception) {
return SSL.newSSLError(runtime, exception);
}
private static RaiseException newSSLError(Ruby runtime, String message) {
return SSL.newSSLError(runtime, message);
}
private static RaiseException newSSLErrorFromHandshake(Ruby runtime, SSLHandshakeException exception) {
// SSLHandshakeException is always a wrap around another exception that
// is the actual cause. In some cases the diagnostic message from the original
// exception is also lost and the handshake exception reads "General SSLEngine problem"
// Follow the cause chain until we get the real message and use that to ensure
// we raise an exception that contains the real reason for failure
Exception cause = exception;
while (cause.getCause() != null && (cause instanceof SSLHandshakeException)) {
cause = (Exception) cause.getCause();
}
return SSL.newSSLError(runtime, cause);
}
private SSLContext sslContext;
private SSLEngine engine;
private RubyIO io;
private ByteBuffer peerAppData;
private ByteBuffer peerNetData;
private ByteBuffer netData;
private ByteBuffer dummy;
private boolean initialHandshake = false;
private SSLEngineResult.HandshakeStatus handshakeStatus;
private SSLEngineResult.Status status;
int verifyResult = X509Utils.V_OK;
@Deprecated
public IRubyObject _initialize(final ThreadContext context,
final IRubyObject[] args, final Block unused) {
return initialize(context, args);
}
@JRubyMethod(name = "initialize", rest = true, frame = true, visibility = Visibility.PRIVATE)
public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
final Ruby runtime = context.runtime;
if ( Arity.checkArgumentCount(runtime, args, 1, 2) == 1 ) {
sslContext = new SSLContext(runtime).initializeImpl();
} else {
sslContext = (SSLContext) args[1];
}
if ( ! ( args[0] instanceof RubyIO ) ) {
throw runtime.newTypeError("IO expected but got " + args[0].getMetaClass().getName());
}
setInstanceVariable("@io", this.io = (RubyIO) args[0]); // compat (we do not read @io)
// Ruby 2.3 : @io.nonblock = true if @io.respond_to?(:nonblock=)
if (io.respondsTo("nonblock=")) {
io.callMethod(context, "nonblock=", runtime.getTrue());
}
setInstanceVariable("@context", this.sslContext); // only compat (we do not use @context)
// This is a bit of a hack: SSLSocket should share code with
// RubyBasicSocket, which always sets sync to true.
// Instead we set it here for now.
this.set_sync(context, runtime.getTrue()); // io.sync = true
this.callMethod(context, "sync_close=", runtime.getFalse());
sslContext.setup(context);
return Utils.invokeSuper(context, this, args, Block.NULL_BLOCK); // super()
}
private SSLEngine ossl_ssl_setup(final ThreadContext context)
throws NoSuchAlgorithmException, KeyManagementException, IOException {
SSLEngine engine = this.engine;
if ( engine != null ) return engine;
// Server Name Indication (SNI) RFC 3546
// SNI support will not be attempted unless hostname is explicitly set by the caller
String peerHost = this.callMethod(context, "hostname").toString();
final int peerPort = socketChannelImpl().getRemotePort();
engine = sslContext.createSSLEngine(peerHost, peerPort);
final javax.net.ssl.SSLSession session = engine.getSession();
peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
netData = ByteBuffer.allocate(session.getPacketBufferSize());
peerNetData.limit(0);
peerAppData.limit(0);
netData.limit(0);
dummy = ByteBuffer.allocate(0);
this.engine = engine;
copySessionSetupIfSet();
return engine;
}
@JRubyMethod(name = "io", alias = "to_io")
public final RubyIO io() { return this.io; }
@JRubyMethod(name = "context")
public final SSLContext context() { return this.sslContext; }
@JRubyMethod(name = "sync")
public IRubyObject sync(final ThreadContext context) {
return this.io.callMethod(context, "sync");
}
@JRubyMethod(name = "sync=")
public IRubyObject set_sync(final ThreadContext context, final IRubyObject sync) {
return this.io.callMethod(context, "sync=", sync);
}
@JRubyMethod
public IRubyObject connect(final ThreadContext context) {
return connectImpl(context, true, true);
}
@JRubyMethod
public IRubyObject connect_nonblock(final ThreadContext context) {
return connectImpl(context, false, true);
}
@JRubyMethod
public IRubyObject connect_nonblock(final ThreadContext context, IRubyObject opts) {
return connectImpl(context, false, getExceptionOpt(context, opts));
}
private IRubyObject connectImpl(final ThreadContext context, final boolean blocking, final boolean exception) {
if ( ! sslContext.isProtocolForClient() ) {
throw newSSLError(context.runtime, "called a function you should not call");
}
try {
if ( ! initialHandshake ) {
SSLEngine engine = ossl_ssl_setup(context);
engine.setUseClientMode(true);
engine.beginHandshake();
handshakeStatus = engine.getHandshakeStatus();
initialHandshake = true;
}
callRenegotiationCallback(context);
final IRubyObject ex = doHandshake(blocking, exception);
if ( ex != null ) return ex; // :wait_readable | :wait_writable
}
catch (SSLHandshakeException e) {
//debugStackTrace(runtime, e);
// unlike server side, client should close outbound channel even if
// we have remaining data to be sent.
forceClose();
throw newSSLErrorFromHandshake(context.runtime, e);
}
catch (NoSuchAlgorithmException e) {
debugStackTrace(context.runtime, e);
forceClose();
throw newSSLError(context.runtime, e);
}
catch (KeyManagementException e) {
debugStackTrace(context.runtime, e);
forceClose();
throw newSSLError(context.runtime, e);
}
catch (IOException e) {
//debugStackTrace(context.runtime, e);
forceClose();
throw newSSLError(context.runtime, e);
}
catch (NotYetConnectedException e) {
throw newErrnoEPIPEError(context.runtime, "SSL_connect");
}
return this;
}
private static RaiseException newErrnoEPIPEError(final Ruby runtime, final String detail) {
return Utils.newError(runtime, runtime.getErrno().getClass("EPIPE"), detail);
}
@JRubyMethod
public IRubyObject accept(final ThreadContext context) {
return acceptImpl(context, true, true);
}
@JRubyMethod
public IRubyObject accept_nonblock(final ThreadContext context) {
return acceptImpl(context, false, true);
}
@JRubyMethod
public IRubyObject accept_nonblock(final ThreadContext context, IRubyObject opts) {
return acceptImpl(context, false, getExceptionOpt(context, opts));
}
@Deprecated
public SSLSocket acceptCommon(ThreadContext context, boolean blocking) {
return (SSLSocket) acceptImpl(context, blocking, true);
}
private IRubyObject acceptImpl(final ThreadContext context, final boolean blocking, final boolean exception) {
if ( ! sslContext.isProtocolForServer() ) {
throw newSSLError(context.runtime, "called a function you should not call");
}
try {
if ( ! initialHandshake ) {
final SSLEngine engine = ossl_ssl_setup(context);
engine.setUseClientMode(false);
final IRubyObject verify_mode = sslContext.callMethod(context, "verify_mode");
if ( ! verify_mode.isNil() ) {
final int verify = RubyNumeric.fix2int(verify_mode);
if ( verify == 0 ) { // VERIFY_NONE
engine.setNeedClientAuth(false);
engine.setWantClientAuth(false);
}
if ( ( verify & 1 ) != 0 ) { // VERIFY_PEER
engine.setWantClientAuth(true);
}
if ( ( verify & 2 ) != 0 ) { // VERIFY_FAIL_IF_NO_PEER_CERT
engine.setNeedClientAuth(true);
}
}
engine.beginHandshake();
handshakeStatus = engine.getHandshakeStatus();
initialHandshake = true;
}
callRenegotiationCallback(context);
final IRubyObject ex = doHandshake(blocking, exception);
if ( ex != null ) return ex; // :wait_readable | :wait_writable
}
catch (SSLHandshakeException e) {
final String msg = e.getMessage();
// updated JDK (>= 1.7.0_75) with deprecated SSL protocols :
// javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
if ( e.getCause() == null && msg != null &&
msg.contains("(protocol is disabled or cipher suites are inappropriate)") ) {
debug(context.runtime, sslContext.getProtocol() + " protocol has been deactivated and is not available by default\n see the java.security.Security property jdk.tls.disabledAlgorithms in <JRE_HOME>/lib/security/java.security file");
}
else {
debugStackTrace(context.runtime, e);
}
throw newSSLErrorFromHandshake(context.runtime, e);
}
catch (NoSuchAlgorithmException e) {
debugStackTrace(context.runtime, e);
throw newSSLError(context.runtime, e);
}
catch (KeyManagementException e) {
debugStackTrace(context.runtime, e);
throw newSSLError(context.runtime, e);
}
catch (IOException e) {
debugStackTrace(context.runtime, e);
throw newSSLError(context.runtime, e);
}
catch (RaiseException e) {
throw e;
}
catch (RuntimeException e) {
debugStackTrace(context.runtime, e);
if ( "Could not generate DH keypair".equals( e.getMessage() ) ) {
throw SSL.handleCouldNotGenerateDHKeyPairError(context.runtime, e);
}
throw newSSLError(context.runtime, e);
}
return this;
}
@JRubyMethod
public IRubyObject verify_result(final ThreadContext context) {
final Ruby runtime = context.runtime;
if (engine == null) {
runtime.getWarnings().warn("SSL session is not started yet.");
return runtime.getNil();
}
return runtime.newFixnum(verifyResult);
}
// This select impl is a copy of RubyThread.select, then blockingLock is
// removed. This impl just set
// SelectableChannel.configureBlocking(false) permanently instead of setting
// temporarily. SSLSocket requires wrapping IO to be selectable so it should
// be OK to set configureBlocking(false) permanently.
private Object waitSelect(final int operations, final boolean blocking, final boolean exception)
throws IOException {
final SocketChannelImpl channel = socketChannelImpl();
if ( ! channel.isSelectable() ) return Boolean.TRUE;
final Ruby runtime = getRuntime();
final RubyThread thread = runtime.getCurrentContext().getThread();
channel.configureBlocking(false);
final Selector selector = runtime.getSelectorPool().get();
final SelectionKey key = channel.register(selector, operations);
try {
final int[] result = new int[1];
if ( ! blocking ) {
try {
result[0] = selector.selectNow();
if ( result[0] == 0 ) {
if ((operations & SelectionKey.OP_READ) != 0 && (operations & SelectionKey.OP_WRITE) != 0) {
if ( key.isReadable() ) {
writeWouldBlock(runtime, exception, result);
}
//else if ( key.isWritable() ) {
// readWouldBlock(runtime, exception, result);
//}
else { //neither, pick one
readWouldBlock(runtime, exception, result);
}
}
else if ((operations & SelectionKey.OP_READ) != 0) {
readWouldBlock(runtime, exception, result);
}
else if ((operations & SelectionKey.OP_WRITE) != 0) {
writeWouldBlock(runtime, exception, result);
}
}
}
catch (IOException ioe) {
throw runtime.newRuntimeError("Error with selector: " + ioe.getMessage());
}
} else {
io.addBlockingThread(thread);
thread.executeBlockingTask(new RubyThread.BlockingTask() {
public void run() throws InterruptedException {
try {
result[0] = selector.select();
}
catch (IOException ioe) {
throw runtime.newRuntimeError("Error with selector: " + ioe.getMessage());
}
}
public void wakeup() {
selector.wakeup();
}
});
}
switch ( result[0] ) {
case READ_WOULD_BLOCK_RESULT :
return runtime.newSymbol("wait_readable"); // exception: false
case WRITE_WOULD_BLOCK_RESULT :
return runtime.newSymbol("wait_writable"); // exception: false
case 0 : return Boolean.FALSE;
default :
//key should always be contained in selectedKeys() here, however there is a bug in
//JRuby <= 9.1.2.0 that makes this not always the case, so we have to check
return selector.selectedKeys().contains(key) ? Boolean.TRUE : Boolean.FALSE;
}
}
catch (InterruptedException interrupt) { return Boolean.FALSE; }
finally {
// Note: I don't like ignoring these exceptions, but it's
// unclear how likely they are to happen or what damage we
// might do by ignoring them. Note that the pieces are separate
// so that we can ensure one failing does not affect the others
// running.
// clean up the key in the selector
try {
if ( key != null ) key.cancel();
if ( selector != null ) selector.selectNow();
}
catch (Exception e) { // ignore
debugStackTrace(runtime, e);
}
// shut down and null out the selector
try {
if ( selector != null ) {
runtime.getSelectorPool().put(selector);
}
}
catch (Exception e) { // ignore
debugStackTrace(runtime, e);
}
if (blocking) {
// remove this thread as a blocker against the given IO
io.removeBlockingThread(thread);
// clear thread state from blocking call
thread.afterBlockingCall();
}
}
}
private static final int READ_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 1;
private static final int WRITE_WOULD_BLOCK_RESULT = Integer.MIN_VALUE + 2;
private static void readWouldBlock(final Ruby runtime, final boolean exception, final int[] result) {
if ( exception ) throw newSSLErrorWaitReadable(runtime, "read would block");
result[0] = READ_WOULD_BLOCK_RESULT;
}
private static void writeWouldBlock(final Ruby runtime, final boolean exception, final int[] result) {
if ( exception ) throw newSSLErrorWaitWritable(runtime, "write would block");
result[0] = WRITE_WOULD_BLOCK_RESULT;
}
private void doHandshake(final boolean blocking) throws IOException {
doHandshake(blocking, true);
}
// might return :wait_readable | :wait_writable in case (true, false)
private IRubyObject doHandshake(final boolean blocking, final boolean exception) throws IOException {
while (true) {
Object sel = waitSelect(SelectionKey.OP_READ | SelectionKey.OP_WRITE, blocking, exception);
if ( sel instanceof IRubyObject ) return (IRubyObject) sel; // :wait_readable | :wait_writable
// if not blocking, raise EAGAIN
if ( ! blocking && sel != Boolean.TRUE ) {
throw getRuntime().newErrnoEAGAINError("Resource temporarily unavailable");
}
// otherwise, proceed as before
switch (handshakeStatus) {
case FINISHED:
case NOT_HANDSHAKING:
if ( initialHandshake ) finishInitialHandshake();
return null; // OK
case NEED_TASK:
doTasks();
break;
case NEED_UNWRAP:
if (readAndUnwrap(blocking) == -1 && handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED) {
throw new SSLHandshakeException("Socket closed");
}
// during initialHandshake, calling readAndUnwrap that results UNDERFLOW
// does not mean writable. we explicitly wait for readable channel to avoid
// busy loop.
if (initialHandshake && status == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
sel = waitSelect(SelectionKey.OP_READ, blocking, exception);
if ( sel instanceof IRubyObject ) return (IRubyObject) sel; // :wait_readable
}
break;
case NEED_WRAP:
if ( netData.hasRemaining() ) {
while ( flushData(blocking) ) { /* loop */ }
}
netData.clear();
SSLEngineResult result = engine.wrap(dummy, netData);
handshakeStatus = result.getHandshakeStatus();
netData.flip();
flushData(blocking);
break;
default:
throw new IllegalStateException("Unknown handshaking status: " + handshakeStatus);
}
}
}
private void doTasks() {
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
task.run();
}
handshakeStatus = engine.getHandshakeStatus();
verifyResult = sslContext.getLastVerifyResult();
}
private boolean flushData(boolean blocking) throws IOException {
try {
writeToChannel(netData, blocking);
}
catch (IOException ioe) {
netData.position(netData.limit());
throw ioe;
}
if ( netData.hasRemaining() ) {
return true;
}
return false;
}
private int writeToChannel(ByteBuffer buffer, boolean blocking) throws IOException {
int totalWritten = 0;
while ( buffer.hasRemaining() ) {
totalWritten += socketChannelImpl().write(buffer);
if ( ! blocking ) break; // don't continue attempting to read
}
return totalWritten;
}
private void finishInitialHandshake() {
initialHandshake = false;
}
private void callRenegotiationCallback(final ThreadContext context) throws RaiseException {
IRubyObject renegotiationCallback = sslContext.getInstanceVariable("@renegotiation_cb");
if(renegotiationCallback == null || renegotiationCallback.isNil()) {
return;
}
else {
// the return of the Proc is not important
// Can throw ruby exception to "disallow" renegotiations
renegotiationCallback.callMethod(context, "call", this);
}
}
public int write(ByteBuffer src, boolean blocking) throws SSLException, IOException {
if ( initialHandshake ) {
throw new IOException("Writing not possible during handshake");
}
SocketChannelImpl channel = socketChannelImpl();
final boolean blockingMode = channel.isBlocking();
if ( ! blocking ) channel.configureBlocking(false);
try {
if ( netData.hasRemaining() ) {
flushData(blocking);
}
netData.clear();
final SSLEngineResult result = engine.wrap(src, netData);
if ( result.getStatus() == SSLEngineResult.Status.CLOSED ) {
throw getRuntime().newIOError("closed SSL engine");
}
netData.flip();
flushData(blocking);
return result.bytesConsumed();
}
finally {
if ( ! blocking ) channel.configureBlocking(blockingMode);
}
}
public int read(final ByteBuffer dst, final boolean blocking) throws IOException {
if ( initialHandshake ) return 0;
if ( engine.isInboundDone() ) return -1;
if ( ! peerAppData.hasRemaining() ) {
int appBytesProduced = readAndUnwrap(blocking);
if (appBytesProduced == -1 || appBytesProduced == 0) {
return appBytesProduced;
}
}
int limit = Math.min(peerAppData.remaining(), dst.remaining());
peerAppData.get(dst.array(), dst.arrayOffset(), limit);
dst.position(dst.arrayOffset() + limit);
return limit;
}
private int readAndUnwrap(final boolean blocking) throws IOException {
final int bytesRead = socketChannelImpl().read(peerNetData);
if ( bytesRead == -1 ) {
if ( ! peerNetData.hasRemaining() ||
( status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) ) {
closeInbound();
return -1;
}
// inbound channel has been already closed but closeInbound() must
// be defered till the last engine.unwrap() call.
// peerNetData could not be empty.
}
peerAppData.clear();
peerNetData.flip();
SSLEngineResult result;
do {
result = engine.unwrap(peerNetData, peerAppData);
}
while ( result.getStatus() == SSLEngineResult.Status.OK &&
result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP &&
result.bytesProduced() == 0 );
if ( result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED ) {
finishInitialHandshake();
}
if ( peerAppData.position() == 0 &&
result.getStatus() == SSLEngineResult.Status.OK &&
peerNetData.hasRemaining() ) {
result = engine.unwrap(peerNetData, peerAppData);
}
status = result.getStatus();
handshakeStatus = result.getHandshakeStatus();
if ( bytesRead == -1 && ! peerNetData.hasRemaining() ) {
// now it's safe to call closeInbound().
closeInbound();
}
if ( status == SSLEngineResult.Status.CLOSED ) {
doShutdown();
return -1;
}
peerNetData.compact();
peerAppData.flip();
if ( ! initialHandshake && (
handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
handshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED ) ) {
doHandshake(blocking);
}
return peerAppData.remaining();
}
private void closeInbound() {
try {
engine.closeInbound();
}
catch (SSLException e) {
debug(getRuntime(), "SSLSocket.closeInbound", e);
// ignore any error on close. possibly an error like this;
// Inbound closed before receiving peer's close_notify: possible truncation attack?
}
}
private void doShutdown() throws IOException {
if ( engine.isOutboundDone() ) return;
netData.clear();
try {
engine.wrap(dummy, netData);
}
catch (SSLException e) {
debug(getRuntime(), "SSLSocket.doShutdown", e);
return;
}
catch (RuntimeException e) {
debugStackTrace(getRuntime(), e);
return;
}
netData.flip();
flushData(true);
}
private IRubyObject sysreadImpl(final ThreadContext context,
IRubyObject len, IRubyObject buff, final boolean blocking, final boolean exception) {
final Ruby runtime = context.runtime;
final int length = RubyNumeric.fix2int(len);
final RubyString buffStr;
if ( buff != null && ! buff.isNil() ) {
buffStr = buff.asString();
} else {
buffStr = RubyString.newEmptyString(runtime); // fine since we're setValue
}
if ( length == 0 ) {
buffStr.clear();
return buffStr;
}
if ( length < 0 ) {
throw runtime.newArgumentError("negative string size (or size too big)");
}
try {
// So we need to make sure to only block when there is no data left to process
if ( engine == null || ! ( peerAppData.hasRemaining() || peerNetData.position() > 0 ) ) {
final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception);
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable
}
final ByteBuffer dst = ByteBuffer.allocate(length);
int read = -1;
// ensure >0 bytes read; sysread is blocking read.
while ( read <= 0 ) {
if ( engine == null ) {
read = socketChannelImpl().read(dst);
} else {
read = read(dst, blocking);
}
if ( read == -1 ) {
if ( exception ) throw runtime.newEOFError();
return runtime.getNil();
}
if ( read == 0 && status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) {
// If we didn't get any data back because we only read in a partial TLS record,
// instead of spinning until the rest comes in, call waitSelect to either block
// until the rest is available, or throw a "read would block" error if we are in
// non-blocking mode.
final Object ex = waitSelect(SelectionKey.OP_READ, blocking, exception);
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_readable
}
}
final byte[] bytesRead = dst.array();
final int offset = dst.position() - read;
buffStr.setValue(new ByteList(bytesRead, offset, read, false));
return buffStr;
}
catch (IOException ioe) {
throw runtime.newIOError(ioe.getMessage());
}
}
@JRubyMethod
public IRubyObject sysread(ThreadContext context, IRubyObject len) {
return sysreadImpl(context, len, null, true, true);
}
@JRubyMethod
public IRubyObject sysread(ThreadContext context, IRubyObject len, IRubyObject buff) {
return sysreadImpl(context, len, buff, true, true);
}
@Deprecated // @JRubyMethod(rest = true, required = 1, optional = 1)
public IRubyObject sysread(ThreadContext context, IRubyObject[] args) {
switch ( args.length) {
case 1 :
return sysread(context, args[0]);
case 2 :
return sysread(context, args[0], args[1]);
}
Arity.checkArgumentCount(context.runtime, args.length, 1, 2);
return null; // won't happen as checkArgumentCount raises
}
@JRubyMethod
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len) {
return sysreadImpl(context, len, null, false, true);
}
@JRubyMethod
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject arg) {
if ( arg instanceof RubyHash ) { // exception: false
// NOTE: on Ruby 2.3 this is expected to raise a TypeError (but not on 2.2)
return sysreadImpl(context, len, null, false, getExceptionOpt(context, arg));
}
return sysreadImpl(context, len, arg, false, true); // buffer arg
}
@JRubyMethod
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject len, IRubyObject buff, IRubyObject opts) {
return sysreadImpl(context, len, buff, false, getExceptionOpt(context, opts));
}
@Deprecated // @JRubyMethod(rest = true, required = 1, optional = 2)
public IRubyObject sysread_nonblock(ThreadContext context, IRubyObject[] args) {
switch ( args.length) {
case 1 :
return sysread_nonblock(context, args[0]);
case 2 :
return sysread_nonblock(context, args[0], args[1]);
case 3 :
return sysread_nonblock(context, args[0], args[1], args[2]);
}
Arity.checkArgumentCount(context.runtime, args.length, 1, 3);
return null; // won't happen as checkArgumentCount raises
}
private IRubyObject syswriteImpl(final ThreadContext context,
final IRubyObject arg, final boolean blocking, final boolean exception) {
final Ruby runtime = context.runtime;
try {
checkClosed();
final Object ex = waitSelect(SelectionKey.OP_WRITE, blocking, exception);
if ( ex instanceof IRubyObject ) return (IRubyObject) ex; // :wait_writable
ByteList bytes = arg.asString().getByteList();
ByteBuffer buff = ByteBuffer.wrap(bytes.getUnsafeBytes(), bytes.getBegin(), bytes.getRealSize());
final int written;
if ( engine == null ) {
written = writeToChannel(buff, blocking);
} else {
written = write(buff, blocking);
}
this.io.callMethod(context, "flush");
return runtime.newFixnum(written);
}
catch (IOException ioe) {
throw runtime.newIOError(ioe.getMessage());
}
}
@JRubyMethod
public IRubyObject syswrite(ThreadContext context, IRubyObject arg) {
return syswriteImpl(context, arg, true, true);
}
@JRubyMethod
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg) {
return syswriteImpl(context, arg, false, true);
}
@JRubyMethod
public IRubyObject syswrite_nonblock(ThreadContext context, IRubyObject arg, IRubyObject opts) {
return syswriteImpl(context, arg, false, getExceptionOpt(context, opts));
}
private static boolean getExceptionOpt(final ThreadContext context, final IRubyObject opts) {
if ( opts instanceof RubyHash ) { // exception: true
final Ruby runtime = context.runtime;
IRubyObject exc = ((RubyHash) opts).op_aref(context, runtime.newSymbol("exception"));
return exc != runtime.getFalse();
}
return true;
}
private void checkClosed() {
if ( ! socketChannelImpl().isOpen() ) {
throw getRuntime().newIOError("closed stream");
}
}
// do shutdown even if we have remaining data to be sent.
// call this when you get an exception from client side.
private void forceClose() {
close(true);
}
private void close(boolean force) {
if ( engine == null ) {
// if ( force ) throw getRuntime().newEOFError();
return;
}
engine.closeOutbound();
if ( ! force && netData.hasRemaining() ) return;
try {
doShutdown();
}
catch (IOException e) { // ignore?
debug(getRuntime(), "SSLSocket.close doShutdown failed", e);
}
catch (NotYetConnectedException e) {
debug(getRuntime(), "SSLSocket.close doShutdown failed", e);
}
}
@JRubyMethod
public IRubyObject sysclose(final ThreadContext context) {
//if ( isClosed() ) return context.runtime.getNil();
if ( this.io.callMethod(context, "closed?").isTrue() ) {
return context.runtime.getNil();
} // Ruby 2.3
// no need to try shutdown when it's a server
close( sslContext.isProtocolForClient() );
if ( this.callMethod(context, "sync_close").isTrue() ) {
return this.io.callMethod(context, "close");
}
return context.runtime.getNil();
}
@JRubyMethod
public IRubyObject cert(final ThreadContext context) {
final Ruby runtime = context.runtime;
if ( engine == null ) return runtime.getNil();
try {
Certificate[] cert = engine.getSession().getLocalCertificates();
if ( cert != null && cert.length > 0 ) {
return X509Cert.wrap(context, cert[0]);
}
}
catch (CertificateEncodingException e) {
throw X509Cert.newCertificateError(runtime, e);
}
return runtime.getNil();
}
// @Deprecated
public final IRubyObject cert() {
return cert(getRuntime().getCurrentContext());
}
@JRubyMethod
public IRubyObject peer_cert(final ThreadContext context) {
final Ruby runtime = context.runtime;
if ( engine == null ) return runtime.getNil();
try {
Certificate[] cert = engine.getSession().getPeerCertificates();
if ( cert.length > 0 ) {
return X509Cert.wrap(context, cert[0]);
}
}
catch (CertificateEncodingException e) {
throw X509Cert.newCertificateError(runtime, e);
}
catch (SSLPeerUnverifiedException e) {
if (OpenSSL.isDebug(runtime)) {
runtime.getWarnings().warning(String.format("%s: %s", e.getClass().getName(), e.getMessage()));
}
}
return runtime.getNil();
}
// @Deprecated
public final IRubyObject peer_cert() {
return peer_cert(getRuntime().getCurrentContext());
}
@JRubyMethod
public IRubyObject peer_cert_chain(final ThreadContext context) {
final Ruby runtime = context.runtime;
if ( engine == null ) return runtime.getNil();
try {
javax.security.cert.Certificate[] certs = engine.getSession().getPeerCertificateChain();
IRubyObject[] cert_chain = new IRubyObject[ certs.length ];
for ( int i = 0; i < certs.length; i++ ) {
cert_chain[i] = X509Cert.wrap(context, certs[i]);
}
return runtime.newArrayNoCopy(cert_chain);
}
catch (javax.security.cert.CertificateEncodingException e) {
throw X509Cert.newCertificateError(getRuntime(), e);
}
catch (SSLPeerUnverifiedException e) {
if (runtime.isVerbose() || OpenSSL.isDebug(runtime)) {
runtime.getWarnings().warning(String.format("%s: %s", e.getClass().getName(), e.getMessage()));
}
}
return runtime.getNil();
}
// @Deprecated
public final IRubyObject peer_cert_chain() {
return peer_cert_chain(getRuntime().getCurrentContext());
}
@JRubyMethod
public IRubyObject cipher() {
if ( engine == null ) return getRuntime().getNil();
return getRuntime().newString( engine.getSession().getCipherSuite() );
}
@JRubyMethod
public IRubyObject npn_protocol() {
if ( engine == null ) return getRuntime().getNil();
// NOTE: maybe a time to use https://github.com/benmmurphy/ssl_npn
warn(getRuntime().getCurrentContext(), "WARNING: SSLSocket#npn_protocol is not supported");
return getRuntime().getNil(); // throw new UnsupportedOperationException();
}
@JRubyMethod
public IRubyObject state() {
warn(getRuntime().getCurrentContext(), "WARNING: unimplemented method called: SSLSocket#state");
return getRuntime().getNil();
}
@JRubyMethod
public IRubyObject pending() {
warn(getRuntime().getCurrentContext(), "WARNING: unimplemented method called: SSLSocket#pending");
return getRuntime().getNil();
}
private boolean reusableSSLEngine() {
if ( engine != null ) {
final String peerHost = engine.getPeerHost();
if ( peerHost != null && peerHost.length() > 0 ) {
// NOT getSSLContext().createSSLEngine() - no hints for session reuse
return true;
}
}
return false;
}
@JRubyMethod(name = "session_reused?")
public IRubyObject session_reused_p() {
if ( reusableSSLEngine() ) {
if ( ! engine.getEnableSessionCreation() ) {
// if session creation is disabled we can be sure its to be re-used
return getRuntime().getTrue();
}
//return getRuntime().getFalse(); // NOTE: likely incorrect (we can not decide)
}
//warn(getRuntime().getCurrentContext(), "WARNING: SSLSocket#session_reused? is not supported");
return getRuntime().getNil(); // can not decide - probably not
}
// JSSE: SSL Sessions can be reused only if connecting to the same host at the same port
final javax.net.ssl.SSLSession sslSession() {
return engine == null ? null : engine.getSession();
}
private transient SSLSession session;
@JRubyMethod(name = "session")
public IRubyObject session(final ThreadContext context) {
if ( sslSession() == null ) return context.nil;
return getSession(context.runtime);
}
private SSLSession getSession(final Ruby runtime) {
if ( session == null ) {
return session = new SSLSession(runtime).initializeImpl(this);
}
return session;
}
private transient SSLSession setSession = null;
@JRubyMethod(name = "session=")
public IRubyObject set_session(IRubyObject session) {
// NOTE: we can not fully support this without the SSL provider internals
// but we can assume setting a session= is meant as a forced session re-use
if ( session instanceof SSLSession ) {
setSession = (SSLSession) session;
if ( engine != null ) copySessionSetupIfSet();
}
//warn(context, "WARNING: SSLSocket#session= has not effect");
return getRuntime().getNil();
}
private void copySessionSetupIfSet() {
if ( setSession != null ) {
if ( reusableSSLEngine() ) {
engine.setEnableSessionCreation(false);
final ThreadContext context = getRuntime().getCurrentContext();
if ( ! setSession.equals( getSession(context.runtime) ) ) {
getSession(context.runtime).set_timeout(context, setSession.timeout(context));
}
}
}
}
@JRubyMethod
public IRubyObject ssl_version() {
if ( engine == null ) return getRuntime().getNil();
return getRuntime().newString( engine.getSession().getProtocol() );
}
private transient SocketChannelImpl socketChannel;
private SocketChannelImpl socketChannelImpl() {
if ( socketChannel != null ) return socketChannel;
final Channel channel = io.getChannel();
if ( channel instanceof SocketChannel ) {
return socketChannel = new JavaSocketChannel((SocketChannel) channel);
}
// TODO JNR
throw new IllegalStateException("unknow channel impl: " + channel + " of type " + channel.getClass().getName());
}
private interface SocketChannelImpl {
boolean isOpen() ;
int read(ByteBuffer dst) throws IOException ;
int write(ByteBuffer src) throws IOException ;
int getRemotePort();
boolean isSelectable() ;
// SelectableChannel
boolean isBlocking() ;
void configureBlocking(boolean block) throws IOException ;
SelectionKey register(Selector selector, int ops) throws IOException ;
//boolean selectionOpsReadable(final int readyOps);
//boolean selectionOpsWritable(final int readyOps) ;
}
private static final class JavaSocketChannel implements SocketChannelImpl {
JavaSocketChannel(final SocketChannel channel) {
this.channel = channel;
}
private final SocketChannel channel;
public boolean isOpen() { return channel.isOpen(); }
public int read(ByteBuffer dst) throws IOException {
return channel.read(dst);
}
public int write(ByteBuffer src) throws IOException {
return channel.write(src);
}
public int getRemotePort() { return channel.socket().getPort(); }
public boolean isSelectable() {
return true; // return channel instanceof SelectableChannel;
}
public boolean isBlocking() { return channel.isBlocking(); }
public void configureBlocking(boolean block) throws IOException {
channel.configureBlocking(block);
}
public SelectionKey register(Selector selector, int ops) throws ClosedChannelException {
return channel.register(selector, ops);
}
public boolean selectionOpsReadable(final int readyOps) {
return (readyOps & SelectionKey.OP_READ) != 0;
}
public boolean selectionOpsWritable(final int readyOps) {
return (readyOps & SelectionKey.OP_WRITE) != 0;
}
}
private static boolean jnrChannel(final Channel channel) {
return channel.getClass().getName().startsWith("jnr.");
}
}// SSLSocket